Build an OwnershipGuard that loads the resource and compares its owner ID against the authenticated user's ID. Admins bypass the check via a role shortcut. Pair the guard with service-level query scoping as defense-in-depth — never rely solely on the guard.
Never rely solely on the guard — always scope database queries to the authenticated user as defense-in-depth.
Load the resource inside the guard to check ownership — do not trust the client to send the owner ID.
Throw NotFoundException (not ForbiddenException) when the resource does not exist — prevents resource enumeration.
Admins typically bypass ownership checks — check role first to avoid an unnecessary database query.
Consider caching the loaded resource in the request so the controller handler does not need to re-fetch it.